<template>
  <div
    class="vdr"
    :style="style"
    :class="active || isActive ? 'active' : 'inactive'"
    @mousedown="bodyDown($event)"
    @touchstart="bodyDown($event)"
    @touchend="up($event)"
  >
    <slot></slot>
    <div
      v-for="(stick,index) in sticks"
      :key="index"
      class="vdr-stick"
      :class="['vdr-stick-' + stick, isResizable ? '' : 'not-resizable']"
      @mousedown.stop.prevent="stickDown(stick, $event)"
      @touchstart.stop.prevent="stickDown(stick, $event)"
      :style="vdrStick(stick)"
    ></div>
  </div>
</template>

<script>
const stickSize = 8;
const styleMapping = {
  y: {
    t: "top",
    m: "marginTop",
    b: "bottom"
  },
  x: {
    l: "left",
    m: "marginLeft",
    r: "right"
  }
};

export default {
  name: "VueDragResize",
  props: {
    parentScaleX: {
      type: Number,
      default: 1
    },
    parentScaleY: {
      type: Number,
      default: 1
    },
    isActive: {
      type: Boolean,
      default: false
    },
    preventActiveBehavior: {
      type: Boolean,
      default: false
    },
    isDraggable: {
      type: Boolean,
      default: true
    },
    isResizable: {
      type: Boolean,
      default: true
    },
    aspectRatio: {
      type: Boolean,
      default: false
    },
    parentLimitation: {
      type: Boolean,
      default: false
    },
    snapToGrid: {
      type: Boolean,
      default: false
    },
    gridX: {
      type: Number,
      default: 50,
      validator: function(val) {
        return val > 0;
      }
    },
    gridY: {
      type: Number,
      default: 50,
      validator: function(val) {
        return val > 0;
      }
    },
    parentW: {
      type: Number,
      default: 0,
      validator: function(val) {
        return val >= 0;
      }
    },
    parentH: {
      type: Number,
      default: 0,
      validator: function(val) {
        return val >= 0;
      }
    },
    w: {
      type: Number,
      default: 100,
      validator: function(val) {
        return val > 0;
      }
    },
    h: {
      type: Number,
      default: 100,
      validator: function(val) {
        return val > 0;
      }
    },
    minw: {
      type: Number,
      default: 50,
      validator: function(val) {
        return val > 0;
      }
    },
    minh: {
      type: Number,
      default: 50,
      validator: function(val) {
        return val > 0;
      }
    },
    x: {
      type: Number,
      default: 0,
      validator: function(val) {
        return typeof val === "number";
      }
    },
    y: {
      type: Number,
      default: 0,
      validator: function(val) {
        return typeof val === "number";
      }
    },
    z: {
      type: [String, Number],
      default: "auto",
      validator: function(val) {
        let valid = typeof val === "string" ? val === "auto" : val >= 0;
        return valid;
      }
    },
    dragHandle: {
      type: String,
      default: null
    },
    dragCancel: {
      type: String,
      default: null
    },
    sticks: {
      type: Array,
      default: function() {
        return ["tl", "tm", "tr", "mr", "br", "bm", "bl", "ml"];
      }
    },
    axis: {
      type: String,
      default: "both",
      validator: function(val) {
        return ["x", "y", "both", "none"].indexOf(val) !== -1;
      }
    }
  },

  data: function() {
    return {
      active: this.isActive,
      rawWidth: this.w,
      rawHeight: this.h,
      rawLeft: this.x,
      rawTop: this.y,
      rawRight: null,
      rawBottom: null,
      zIndex: this.z,
      aspectFactor: this.w / this.h,
      parentWidth: null,
      parentHeight: null,
      left: this.x,
      top: this.y,
      right: null,
      bottom: null,
      minWidth: this.minw,
      minHeight: this.minh
    };
  },

  created: function() {
    this.stickDrag = false;
    this.bodyDrag = false;
    this.stickAxis = null;
    this.stickStartPos = { mouseX: 0, mouseY: 0, x: 0, y: 0, w: 0, h: 0 };
    this.limits = {
      minLeft: null,
      maxLeft: null,
      minRight: null,
      maxRight: null,
      minTop: null,
      maxTop: null,
      minBottom: null,
      maxBottom: null
    };

    this.currentStick = [];
  },

  mounted: function() {
    this.parentElement = this.$el.parentNode;
    this.parentWidth = this.parentW
      ? this.parentW
      : this.parentElement.clientWidth;
    this.parentHeight = this.parentH
      ? this.parentH
      : this.parentElement.clientHeight;

    this.rawRight = this.parentWidth - this.rawWidth - this.rawLeft;
    this.rawBottom = this.parentHeight - this.rawHeight - this.rawTop;

    document.documentElement.addEventListener("mousemove", this.move);
    document.documentElement.addEventListener("mouseup", this.up);
    document.documentElement.addEventListener("mouseleave", this.up);

    // document.documentElement.addEventListener("mousedown", this.deselect);

    document.documentElement.addEventListener("touchmove", this.move, true);
    document.documentElement.addEventListener(
      "touchend touchcancel",
      this.up,
      true
    );
    document.documentElement.addEventListener("touchstart", this.up, true);

    if (this.dragHandle) {
      let dragHandles = Array.prototype.slice.call(
        this.$el.querySelectorAll(this.dragHandle)
      );
      for (let i in dragHandles) {
        dragHandles[i].setAttribute("data-drag-handle", this._uid);
      }
    }

    if (this.dragCancel) {
      let cancelHandles = Array.prototype.slice.call(
        this.$el.querySelectorAll(this.dragCancel)
      );
      for (let i in cancelHandles) {
        cancelHandles[i].setAttribute("data-drag-cancel", this._uid);
      }
    }
  },

  beforeDestroy: function() {
    document.documentElement.removeEventListener("mousemove", this.move);
    document.documentElement.removeEventListener("mouseup", this.up);
    document.documentElement.removeEventListener("mouseleave", this.up);

    // document.documentElement.removeEventListener("mousedown", this.deselect);

    document.documentElement.removeEventListener("touchmove", this.move, true);
    document.documentElement.removeEventListener(
      "touchend touchcancel",
      this.up,
      true
    );
    document.documentElement.removeEventListener("touchstart", this.up, true);
  },

  methods: {
    deselect() {
      if (this.preventActiveBehavior) {
        return;
      }
      this.active = false;
    },

    move(ev) {
      if (!this.stickDrag && !this.bodyDrag) {
        return;
      }

      ev.stopPropagation();

      if (this.stickDrag) {
        this.stickMove(ev);
      }
      if (this.bodyDrag) {
        this.bodyMove(ev);
      }
    },

    up(ev) {
      if (this.stickDrag) {
        this.stickUp(ev);
      }
      if (this.bodyDrag) {
        this.bodyUp(ev);
      }
    },

    bodyDown: function(ev) {
      let target = ev.target || ev.srcElement;

      if (!this.preventActiveBehavior) {
        this.active = true;
      }

      if (ev.button && ev.button !== 0) {
        return;
      }

      this.$emit("clicked", ev);

      if (!this.isDraggable || !this.active) {
        return;
      }

      if (
        this.dragHandle &&
        target.getAttribute("data-drag-handle") !== this._uid.toString()
      ) {
        return;
      }

      if (
        this.dragCancel &&
        target.getAttribute("data-drag-cancel") === this._uid.toString()
      ) {
        return;
      }

      ev.stopPropagation();
      ev.preventDefault();

      this.bodyDrag = true;

      this.stickStartPos.mouseX =
        typeof ev.pageX !== "undefined" ? ev.pageX : ev.touches[0].pageX;
      this.stickStartPos.mouseY =
        typeof ev.pageY !== "undefined" ? ev.pageY : ev.touches[0].pageY;

      this.stickStartPos.left = this.left;
      this.stickStartPos.right = this.right;
      this.stickStartPos.top = this.top;
      this.stickStartPos.bottom = this.bottom;

      if (this.parentLimitation) {
        this.limits = this.calcDragLimitation();
      }
    },

    calcDragLimitation() {
      const parentWidth = this.parentWidth;
      const parentHeight = this.parentHeight;

      return {
        minLeft: 0,
        maxLeft: parentWidth - this.width,
        minRight: 0,
        maxRight: parentWidth - this.width,
        minTop: 0,
        maxTop: parentHeight - this.height,
        minBottom: 0,
        maxBottom: parentHeight - this.height
      };
    },

    bodyMove(ev) {
      const stickStartPos = this.stickStartPos;
      const parentWidth = this.parentWidth;
      const parentHeight = this.parentHeight;
      const gridX = this.gridX;
      const gridY = this.gridY;
      const width = this.width;
      const height = this.height;
      const pageX =
        typeof ev.pageX !== "undefined" ? ev.pageX : ev.touches[0].pageX;
      const pageY =
        typeof ev.pageY !== "undefined" ? ev.pageY : ev.touches[0].pageY;

      let delta = {
        x:
          (this.axis !== "y" && this.axis !== "none"
            ? stickStartPos.mouseX - pageX
            : 0) / this.parentScaleX,
        y:
          (this.axis !== "x" && this.axis !== "none"
            ? stickStartPos.mouseY - pageY
            : 0) / this.parentScaleY
      };

      let newTop = stickStartPos.top - delta.y;
      let newBottom = stickStartPos.bottom + delta.y;
      let newLeft = stickStartPos.left - delta.x;
      let newRight = stickStartPos.right + delta.x;

      if (this.snapToGrid) {
        let alignTop = true;
        let alignLeft = true;

        let diffT = newTop - Math.floor(newTop / gridY) * gridY;
        let diffB =
          parentHeight -
          newBottom -
          Math.floor((parentHeight - newBottom) / gridY) * gridY;
        let diffL = newLeft - Math.floor(newLeft / gridX) * gridX;
        let diffR =
          parentWidth -
          newRight -
          Math.floor((parentWidth - newRight) / gridX) * gridX;

        if (diffT > gridY / 2) {
          diffT = diffT - gridY;
        }
        if (diffB > gridY / 2) {
          diffB = diffB - gridY;
        }
        if (diffL > gridX / 2) {
          diffL = diffL - gridX;
        }
        if (diffR > gridX / 2) {
          diffR = diffR - gridX;
        }

        if (Math.abs(diffB) < Math.abs(diffT)) {
          alignTop = false;
        }
        if (Math.abs(diffR) < Math.abs(diffL)) {
          alignLeft = false;
        }

        newTop = newTop - (alignTop ? diffT : diffB);
        newBottom = parentHeight - height - newTop;
        newLeft = newLeft - (alignLeft ? diffL : diffR);
        newRight = parentWidth - width - newLeft;
      }

      this.rawTop = newTop;
      this.rawBottom = newBottom;
      this.rawLeft = newLeft;
      this.rawRight = newRight;
      this.$emit("dragging", this.rect);
    },

    bodyUp() {
      this.bodyDrag = false;
      this.$emit("dragging", this.rect);
      this.$emit("dragstop", this.rect);

      this.stickStartPos = { mouseX: 0, mouseY: 0, x: 0, y: 0, w: 0, h: 0 };
      this.limits = {
        minLeft: null,
        maxLeft: null,
        minRight: null,
        maxRight: null,
        minTop: null,
        maxTop: null,
        minBottom: null,
        maxBottom: null
      };
    },

    stickDown: function(stick, ev) {
      if (!this.isResizable || !this.active) {
        return;
      }

      this.stickDrag = true;
      this.stickStartPos.mouseX =
        typeof ev.pageX !== "undefined" ? ev.pageX : ev.touches[0].pageX;
      this.stickStartPos.mouseY =
        typeof ev.pageY !== "undefined" ? ev.pageY : ev.touches[0].pageY;
      this.stickStartPos.left = this.left;
      this.stickStartPos.right = this.right;
      this.stickStartPos.top = this.top;
      this.stickStartPos.bottom = this.bottom;
      this.currentStick = stick.split("");
      this.stickAxis = null;

      switch (this.currentStick[0]) {
        case "b":
          this.stickAxis = "y";
          break;
        case "t":
          this.stickAxis = "y";
          break;
      }
      switch (this.currentStick[1]) {
        case "r":
          this.stickAxis = this.stickAxis === "y" ? "xy" : "x";
          break;
        case "l":
          this.stickAxis = this.stickAxis === "y" ? "xy" : "x";
          break;
      }

      this.limits = this.calcResizeLimitation();
    },

    calcResizeLimitation() {
      let minw = this.minWidth;
      let minh = this.minHeight;
      const aspectFactor = this.aspectFactor;
      const width = this.width;
      const height = this.height;
      const bottom = this.bottom;
      const top = this.top;
      const left = this.left;
      const right = this.right;
      const stickAxis = this.stickAxis;

      const parentLim = this.parentLimitation ? 0 : null;

      if (this.aspectRatio) {
        if (minw / minh > aspectFactor) {
          minh = minw / aspectFactor;
        } else {
          minw = aspectFactor * minh;
        }
      }

      let limits = {
        minLeft: parentLim,
        maxLeft: left + (width - minw),
        minRight: parentLim,
        maxRight: right + (width - minw),
        minTop: parentLim,
        maxTop: top + (height - minh),
        minBottom: parentLim,
        maxBottom: bottom + (height - minh)
      };

      if (this.aspectRatio) {
        const aspectLimits = {
          minLeft: left - Math.min(top, bottom) * aspectFactor * 2,
          maxLeft: left + ((height - minh) / 2) * aspectFactor * 2,

          minRight: right - Math.min(top, bottom) * aspectFactor * 2,
          maxRight: right + ((height - minh) / 2) * aspectFactor * 2,

          minTop: top - (Math.min(left, right) / aspectFactor) * 2,
          maxTop: top + ((width - minw) / 2 / aspectFactor) * 2,

          minBottom: bottom - (Math.min(left, right) / aspectFactor) * 2,
          maxBottom: bottom + ((width - minw) / 2 / aspectFactor) * 2
        };

        if (stickAxis === "x") {
          limits = {
            minLeft: Math.max(limits.minLeft, aspectLimits.minLeft),
            maxLeft: Math.min(limits.maxLeft, aspectLimits.maxLeft),
            minRight: Math.max(limits.minRight, aspectLimits.minRight),
            maxRight: Math.min(limits.maxRight, aspectLimits.maxRight)
          };
        } else if (stickAxis === "y") {
          limits = {
            minTop: Math.max(limits.minTop, aspectLimits.minTop),
            maxTop: Math.min(limits.maxTop, aspectLimits.maxTop),
            minBottom: Math.max(limits.minBottom, aspectLimits.minBottom),
            maxBottom: Math.min(limits.maxBottom, aspectLimits.maxBottom)
          };
        }
      }

      return limits;
    },

    stickMove(ev) {
      const stickStartPos = this.stickStartPos;
      const pageX =
        typeof ev.pageX !== "undefined" ? ev.pageX : ev.touches[0].pageX;
      const pageY =
        typeof ev.pageY !== "undefined" ? ev.pageY : ev.touches[0].pageY;

      const delta = {
        x: (stickStartPos.mouseX - pageX) / this.parentScaleX,
        y: (stickStartPos.mouseY - pageY) / this.parentScaleY
      };

      let newTop = stickStartPos.top - delta.y;
      let newBottom = stickStartPos.bottom + delta.y;
      let newLeft = stickStartPos.left - delta.x;
      let newRight = stickStartPos.right + delta.x;

      switch (this.currentStick[0]) {
        case "b":
          if (this.snapToGrid) {
            newBottom =
              this.parentHeight -
              Math.round((this.parentHeight - newBottom) / this.gridY) *
                this.gridY;
          }

          this.rawBottom = newBottom;
          break;

        case "t":
          if (this.snapToGrid) {
            newTop = Math.round(newTop / this.gridY) * this.gridY;
          }

          this.rawTop = newTop;
          break;
      }

      switch (this.currentStick[1]) {
        case "r":
          if (this.snapToGrid) {
            newRight =
              this.parentWidth -
              Math.round((this.parentWidth - newRight) / this.gridX) *
                this.gridX;
          }

          this.rawRight = newRight;
          break;

        case "l":
          if (this.snapToGrid) {
            newLeft = Math.round(newLeft / this.gridX) * this.gridX;
          }

          this.rawLeft = newLeft;
          break;
      }

      this.$emit("resizing", this.rect);
    },

    stickUp() {
      this.stickDrag = false;
      this.stickStartPos = {
        mouseX: 0,
        mouseY: 0,
        x: 0,
        y: 0,
        w: 0,
        h: 0
      };
      this.limits = {
        minLeft: null,
        maxLeft: null,
        minRight: null,
        maxRight: null,
        minTop: null,
        maxTop: null,
        minBottom: null,
        maxBottom: null
      };
      this.rawTop = this.top;
      this.rawBottom = this.bottom;
      this.rawLeft = this.left;
      this.rawRight = this.right;

      this.stickAxis = null;

      this.$emit("resizing", this.rect);
      this.$emit("resizestop", this.rect);
    },

    aspectRatioCorrection() {
      if (!this.aspectRatio) {
        return;
      }

      const bottom = this.bottom;
      const top = this.top;
      const left = this.left;
      const right = this.right;
      const width = this.width;
      const height = this.height;
      const aspectFactor = this.aspectFactor;
      const currentStick = this.currentStick;

      if (width / height > aspectFactor) {
        let newWidth = aspectFactor * height;

        if (currentStick[1] === "l") {
          this.left = left + width - newWidth;
        } else {
          this.right = right + width - newWidth;
        }
      } else {
        let newHeight = width / aspectFactor;

        if (currentStick[0] === "t") {
          this.top = top + height - newHeight;
        } else {
          this.bottom = bottom + height - newHeight;
        }
      }
    }
  },

  computed: {
    style() {
      return {
        top: this.top + "px",
        left: this.left + "px",
        width: this.width + "px",
        height: this.height + "px",
        zIndex: this.zIndex
      };
    },

    vdrStick() {
      return stick => {
        const stickStyle = {
          width: `${stickSize / this.parentScaleX}px`,
          height: `${stickSize / this.parentScaleY}px`
        };
        stickStyle[styleMapping.y[stick[0]]] = `${stickSize /
          this.parentScaleX /
          -2}px`;
        stickStyle[styleMapping.x[stick[1]]] = `${stickSize /
          this.parentScaleX /
          -2}px`;
        return stickStyle;
      };
    },

    width() {
      return this.parentWidth - this.left - this.right;
    },

    height() {
      return this.parentHeight - this.top - this.bottom;
    },

    rect() {
      return {
        left: Math.round(this.left),
        top: Math.round(this.top),
        width: Math.round(this.width),
        height: Math.round(this.height)
      };
    }
  },

  watch: {
    rawLeft(newLeft) {
      const limits = this.limits;
      const stickAxis = this.stickAxis;
      const aspectFactor = this.aspectFactor;
      const aspectRatio = this.aspectRatio;
      const left = this.left;
      const bottom = this.bottom;
      const top = this.top;

      if (limits.minLeft !== null && newLeft < limits.minLeft) {
        newLeft = limits.minLeft;
      } else if (limits.maxLeft !== null && limits.maxLeft < newLeft) {
        newLeft = limits.maxLeft;
      }

      if (aspectRatio && stickAxis === "x") {
        const delta = left - newLeft;
        this.rawTop = top - delta / aspectFactor / 2;
        this.rawBottom = bottom - delta / aspectFactor / 2;
      }

      this.left = newLeft;
    },

    rawRight(newRight) {
      const limits = this.limits;
      const stickAxis = this.stickAxis;
      const aspectFactor = this.aspectFactor;
      const aspectRatio = this.aspectRatio;
      const right = this.right;
      const bottom = this.bottom;
      const top = this.top;

      if (limits.minRight !== null && newRight < limits.minRight) {
        newRight = limits.minRight;
      } else if (limits.maxRight !== null && limits.maxRight < newRight) {
        newRight = limits.maxRight;
      }

      if (aspectRatio && stickAxis === "x") {
        const delta = right - newRight;
        this.rawTop = top - delta / aspectFactor / 2;
        this.rawBottom = bottom - delta / aspectFactor / 2;
      }

      this.right = newRight;
    },

    rawTop(newTop) {
      const limits = this.limits;
      const stickAxis = this.stickAxis;
      const aspectFactor = this.aspectFactor;
      const aspectRatio = this.aspectRatio;
      const right = this.right;
      const left = this.left;
      const top = this.top;

      if (limits.minTop !== null && newTop < limits.minTop) {
        newTop = limits.minTop;
      } else if (limits.maxTop !== null && limits.maxTop < newTop) {
        newTop = limits.maxTop;
      }

      if (aspectRatio && stickAxis === "y") {
        const delta = top - newTop;
        this.rawLeft = left - (delta * aspectFactor) / 2;
        this.rawRight = right - (delta * aspectFactor) / 2;
      }

      this.top = newTop;
    },

    rawBottom(newBottom) {
      const limits = this.limits;
      const stickAxis = this.stickAxis;
      const aspectFactor = this.aspectFactor;
      const aspectRatio = this.aspectRatio;
      const right = this.right;
      const left = this.left;
      const bottom = this.bottom;

      if (limits.minBottom !== null && newBottom < limits.minBottom) {
        newBottom = limits.minBottom;
      } else if (limits.maxBottom !== null && limits.maxBottom < newBottom) {
        newBottom = limits.maxBottom;
      }

      if (aspectRatio && stickAxis === "y") {
        const delta = bottom - newBottom;
        this.rawLeft = left - (delta * aspectFactor) / 2;
        this.rawRight = right - (delta * aspectFactor) / 2;
      }

      this.bottom = newBottom;
    },

    width() {
      this.aspectRatioCorrection();
    },

    height() {
      this.aspectRatioCorrection();
    },

    active(isActive) {
      if (isActive) {
        this.$emit("activated");
      } else {
        this.$emit("deactivated");
      }
    },

    isActive(val) {
      this.active = val;
    },

    z(val) {
      if (val >= 0 || val === "auto") {
        this.zIndex = val;
      }
    },

    aspectRatio(val) {
      if (val) {
        this.aspectFactor = this.width / this.height;
      }
    },

    minw(val) {
      if (val > 0 && val <= this.width) {
        this.minWidth = val;
      }
    },

    minh(val) {
      if (val > 0 && val <= this.height) {
        this.minHeight = val;
      }
    },

    x() {
      if (this.stickDrag || this.bodyDrag) {
        return;
      }
      if (this.parentLimitation) {
        this.limits = this.calcDragLimitation();
      }

      let delta = this.x - this.left;
      this.rawLeft = this.x;
      this.rawRight = this.right - delta;
    },

    y() {
      if (this.stickDrag || this.bodyDrag) {
        return;
      }

      if (this.parentLimitation) {
        this.limits = this.calcDragLimitation();
      }

      let delta = this.y - this.top;
      this.rawTop = this.y;
      this.rawBottom = this.bottom - delta;
    },

    w() {
      if (this.stickDrag || this.bodyDrag) {
        return;
      }

      this.currentStick = ["m", "r"];
      this.stickAxis = "x";

      if (this.parentLimitation) {
        this.limits = this.calcResizeLimitation();
      }

      let delta = this.width - this.w;
      this.rawRight = this.right + delta;
    },

    h() {
      if (this.stickDrag || this.bodyDrag) {
        return;
      }

      this.currentStick = ["b", "m"];
      this.stickAxis = "y";

      if (this.parentLimitation) {
        this.limits = this.calcResizeLimitation();
      }

      let delta = this.height - this.h;
      this.rawBottom = this.bottom + delta;
    },

    parentW(val) {
      this.right = val - this.width - this.left;
      this.parentWidth = val;
    },

    parentH(val) {
      this.bottom = val - this.height - this.top;
      this.parentHeight = val;
    }
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style>
.vdr {
  position: absolute;
  box-sizing: border-box;
}
.vdr.active:before {
  content: "";
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
  box-sizing: border-box;
  outline: 1px dashed #d6d6d6;
}
.vdr-stick {
  box-sizing: border-box;
  position: absolute;
  font-size: 1px;
  background: #ffffff;
  border: 1px solid #6c6c6c;
  box-shadow: 0 0 2px #bbb;
}
.inactive .vdr-stick {
  display: none;
}
.vdr-stick-tl,
.vdr-stick-br {
  cursor: nwse-resize;
}
.vdr-stick-tm,
.vdr-stick-bm {
  left: 50%;
  cursor: ns-resize;
}
.vdr-stick-tr,
.vdr-stick-bl {
  cursor: nesw-resize;
}
.vdr-stick-ml,
.vdr-stick-mr {
  top: 50%;
  cursor: ew-resize;
}
.vdr-stick.not-resizable {
  display: none;
}
</style>
